원본 항목: Part 3. 시간 및 상태 — 3. 심볼릭명이 정확한 대상에 매핑되어 있지 않음CWE-386repo 폴더: sd39_symmap/ (빌드·실행은 §3-1)
목표 시간: 1회차 15분 / 2회차 9분 / 3회차 6분
Part 3의 마지막 드릴이다.
SD37과 공격 표면은 같지만 초점이 다르다 — SD37은 “생성과 검사를 원자화”했다면, 여기서는 **“이름 자체를 예측 불가능하게 만들면 공격자가 애초에 트랩을 심을 좌표를 모른다”**를 확인한다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- snprintf(path, sizeof(path), "%s/guess.tmp", dir); /* 이름이 항상 같다 -> 예측 가능 */- f = fopen(path, "w+"); /* 심볼릭 링크면 그대로 따라간다 */+ snprintf(path, sizeof(path), "%s/tmpXXXXXX", dir);+ fd = mkstemp(path); /* 무작위 접미사 -- 미리 예측 불가 */
공격자가 심볼릭 링크 트랩을 심으려면 그 파일이 정확히 어떤 이름으로 만들어질지 미리 알아야 한다.
이름이 고정이거나 규칙적이면(app.pid, guess.tmp, session_1…) 언제든 예측해서 미리 링크를 걸어둘 수 있다.
mkstemp()는 이름의 일부를 커널이 무작위로 채워 넣기 때문에 “생성되기 전까지는 그 이름을 아무도 모른다.”
회차별 진행
회차
변형
보는 것
순서
1회차
A 임시 작업 파일 생성
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 소켓 수신 데이터를 임시 파일에 스트리밍 저장하는 형태로 변형
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 디렉터리 안의 심볼릭 링크를 사전에 스캔해서 경고 로그 남기기
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
int create_random_tmp_good(const char *dir, char *out_path, size_t out_size, const char *content){ /* [ ] 1. 템플릿 만들기 */ /* [ ] 2. fd = mkstemp(path) ← 이름을 커널이 무작위로 채운다 */ /* [ ] 3. f = fdopen(fd, "w") */ /* [ ] 4. fputs(content, f), fclose(f) */ /* [ ] 5. 결과 경로를 호출자 버퍼로 복사 */ /* [ ] 6. return 0 */}
2-C. create_random_tmp_good — Level 3 (3회차, 백지)
int create_random_tmp_good(const char *dir, char *out_path, size_t out_size,{ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `mkstemp` 템플릿은 끝이 정확히 `XXXXXX` * - `mkstemp` 는 `path` 를 **수정**하므로 상수 문자열을 넘기지 않는다 */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
#define _DEFAULT_SOURCE#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <limits.h>#include "symmap.h"int open_predictable_tmp_bad(const char *dir, const char *content){ char path[PATH_MAX]; FILE *f; snprintf(path, sizeof(path), "%s/guess.tmp", dir); /* 이름이 항상 같다 -> 예측 가능 */ if (access(path, W_OK) != 0) return -1; f = fopen(path, "w+"); /* 심볼릭 링크면 그대로 따라간다 */ if (!f) return -1; fputs(content, f); fclose(f); return 0;}int create_random_tmp_good(const char *dir, char *out_path, size_t out_size, const char *content){ char path[PATH_MAX]; int fd; FILE *f; if (snprintf(path, sizeof(path), "%s/tmpXXXXXX", dir) >= (int)sizeof(path)) return -1; fd = mkstemp(path); /* 무작위 접미사 -- 미리 예측 불가 */ if (fd == -1) return -1; f = fdopen(fd, "w"); if (!f) { close(fd); return -1; } fputs(content, f); fclose(f); if (out_size <= strlen(path)) return -1; snprintf(out_path, out_size, "%s", path); return 0;}
눈여겨볼 점: open_predictable_tmp_bad의 path는 함수를 호출하기 전부터 이미 결정돼 있다(dir 하나만 알면 누구나 계산 가능).
create_random_tmp_good의 path는 mkstemp()내부에서 커널이 결정하고, 호출자는 그 결과를 사후에 받아볼 뿐이다.
“언제 이름이 정해지는가”의 차이가 곧 예측 가능성의 차이다.